// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SettingsFile.cs" company="Roman '4ux-nbIx' Novitsky">
//   Copyright (c) Roman '4ux-nbIx' Novitsky 2009
// </copyright>
// <summary>
//   Describes and manipulates the settings file.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace SettingsManager
{
  #region Namespace Imports

  using System;
  using System.Collections;
  using System.Collections.Generic;
  using System.Globalization;
  using System.IO;
  using System.Linq;
  using System.Text;
  using System.Threading;
  using System.Xml;

  using ComponentSettingsManagers;

  using JetBrains.Annotations;
  using JetBrains.Util;

  using Properties;

  #endregion


  /// <summary>
  /// Describes and manipulates the settings file.
  /// </summary>
  internal class SettingsFile
  {
    #region Constructors and Destructors

    /// <summary>
    /// Initializes a new instance of the <see cref="SettingsFile"/> class.
    /// </summary>
    /// <param name="filePath">
    /// The file path.
    /// </param>
    protected SettingsFile([NotNull] string filePath)
    {
      Path = new FileSystemPath(filePath);
      Name = System.IO.Path.GetFileNameWithoutExtension(filePath);
      SettingsManagers = ResharperSettingsManager.Instance.ComponentSettingsManagers;
      SettingsManagersOptions = new Dictionary<string, object>();
    }

    #endregion


    #region Properties

    /// <summary>
    /// Gets or sets a value indicating whether to auto-update common settings.
    /// </summary>
    /// <value>
    /// <c>True</c> if auto-update common settings is enabled; otherwise, <c>false</c>.
    /// </value>
    public bool AutoUpdateCommonSettings
    {
      get;
      set;
    }

    /// <summary>
    /// Gets or sets a value indicating whether to clear read only flag on updates.
    /// </summary>
    /// <value>
    /// <c>True</c> if the read only flag should be cleared on updates; otherwise, <c>false</c>.
    /// </value>
    public bool ClearReadOnlyFlagOnUpdates
    {
      get;
      set;
    }

    /// <summary>
    /// Gets the full settings container file path.
    /// </summary>
    /// <value>
    /// The full settings container file path.
    /// </value>
    public FileSystemPath ExternalSettingsContainerAbsoluteFilePath
    {
      get
      {
        if (ExternalSettingsContainerFilePath != null)
        {
          return ExternalSettingsContainerFilePath.IsAbsolute
                   ? new FileSystemPath(ExternalSettingsContainerFilePath.FullPath)
                   : Path.Directory.Combine(ExternalSettingsContainerFilePath.FullPath);
        }

        return Path;
      }
    }

    /// <summary>
    /// Gets or sets the external settings container file path.
    /// </summary>
    /// <value>
    /// The external settings container file path.
    /// </value>
    public FileSystemPath ExternalSettingsContainerFilePath
    {
      get;
      set;
    }

    /// <summary>
    /// Gets the settings file name.
    /// </summary>
    /// <value>
    /// The settings file name.
    /// </value>
    public string Name
    {
      get;
      private set;
    }

    /// <summary>
    /// Gets the display name.
    /// </summary>
    public virtual string DisplayName
    {
      get
      {
        return Name;
      }
    }

    /// <summary>
    /// Gets the file path.
    /// </summary>
    /// <value>
    /// The file path.
    /// </value>
    public FileSystemPath Path
    {
      get;
      private set;
    }


    /// <summary>
    /// Gets or sets the settings managers.
    /// </summary>
    /// <value>
    /// The settings managers.
    /// </value>
    [NotNull]
    public IEnumerable<IComponentSettingsManager> SettingsManagers
    {
      get;
      set;
    }


    /// <summary>
    /// Gets the settings managers options.
    /// </summary>
    /// <value>
    /// The settings managers options.
    /// </value>
    public Dictionary<string, object> SettingsManagersOptions
    {
      get;
      private set;
    }

    #endregion


    #region Public Methods

    /// <summary>
    /// Creates the new file at the specified file path.
    /// </summary>
    /// <param name="filePath">
    /// The file path.
    /// </param>
    /// <returns>
    /// Returns an instance of <see cref="SettingsManager.SettingsFile"/>.
    /// </returns>
    /// <exception cref="InvalidOperationException">
    /// A file at the specified path already exists.
    /// </exception>
    [NotNull]
    public static SettingsFile Create([NotNull] string filePath)
    {
      if (File.Exists(filePath))
      {
        throw new InvalidOperationException("A file at the specified path already exists.");
      }

      var settingsFile = new SettingsFile(filePath)
                           {
                              AutoUpdateCommonSettings = true, ClearReadOnlyFlagOnUpdates = false 
                           };
      return settingsFile;
    }


    /// <summary>
    /// Loads the file at the specified file path.
    /// </summary>
    /// <param name="filePath">
    /// The file path.
    /// </param>
    /// <returns>
    /// Returns an instance of <see cref="SettingsManager.SettingsFile"/>.
    /// </returns>
    [CanBeNull]
    public static SettingsFile Load([NotNull] string filePath)
    {
      var file = new SettingsFile(filePath);

      return file.LoadContents() ? file : null;
    }


    /// <summary>
    /// Loads the resharper settings.
    /// </summary>
    /// <returns>
    /// Returns an instance of <see cref="System.Boolean"/>.
    /// </returns>
    public bool LoadResharperSettings()
    {
      return LoadResharperSettings(ResharperSettingsManager.Instance.ComponentSettingsManagers);
    }


    /// <summary>
    /// Loads the resharper settings.
    /// </summary>
    /// <param name="settingsManagers">
    /// The settings managers.
    /// </param>
    /// <returns>
    /// Returns an instance of <see cref="System.Boolean"/>.
    /// </returns>
    public virtual bool LoadResharperSettings([NotNull] IEnumerable<IComponentSettingsManager> settingsManagers)
    {
      Exception exception = null;

      for (int i = 0; i < 3; i++)
      {
        try
        {
          var xmlDocument = new XmlDocument();

          xmlDocument.Load(ExternalSettingsContainerAbsoluteFilePath.FullPath);

          XmlElement resharperSettingsNode = xmlDocument.GetResharperSettingsNode();

          if (resharperSettingsNode == null)
          {
            return true;
          }

          settingsManagers = settingsManagers.Where(m => SettingsManagers.Contains(m));
          ResharperSettingsManager.Instance.LoadResharperSettings(resharperSettingsNode, settingsManagers, SettingsManagersOptions);

          exception = null;

          break;
        }
        catch (IOException ex)
        {
          exception = ex;
        }
        catch (Exception ex)
        {
          exception = ex;

          break;
        }

        Thread.Sleep(new TimeSpan(0, 0, 0, 3));
      }

      if (exception == null)
      {
        return true;
      }

      MessageBoxExtensions.ShowError(exception, "Error", "An error occurred while loading settings file.");

      return false;
    }


    /// <summary>
    /// Saves the contents.
    /// </summary>
    /// <returns>
    /// Returns an instance of <see cref="System.Boolean"/>.
    /// </returns>
    public bool SaveContents()
    {
      try
      {
        var document = new XmlDocument();

        if (File.Exists(Path.FullPath))
        {
          document.Load(Path.FullPath);
        }

        XmlElement settingsElement = GetSharingSettingsNode(document);

        // version
        XmlAttribute versionAttribute = document.CreateAttribute(Resources.SettingsFileRootNodeVersionAttributeName);
        versionAttribute.Value = Resources.SettingsFileVersion;
        settingsElement.Attributes.Append(versionAttribute);

        // autoupdate common settings
        XmlAttribute autoUpdateAttribute = document.CreateAttribute(Resources.AutoUpdateCommonSettingsAttributeName);
        autoUpdateAttribute.Value = AutoUpdateCommonSettings.ToString();
        settingsElement.Attributes.Append(autoUpdateAttribute);

        // clear read only flag
        XmlAttribute clearReadOnlyFlagAttribute =
          document.CreateAttribute(Resources.ClearReadOnlyFlagOnUpdatesAttributeName);
        clearReadOnlyFlagAttribute.Value = ClearReadOnlyFlagOnUpdates.ToString();
        settingsElement.Attributes.Append(clearReadOnlyFlagAttribute);

        // external resharper settings location
        if (ExternalSettingsContainerFilePath != null)
        {
          XmlAttribute externalFileAttribute = document.CreateAttribute(Resources.ExternalSettingsFilePathAttributeName);
          externalFileAttribute.Value = ExternalSettingsContainerFilePath.FullPath;
          settingsElement.Attributes.Append(externalFileAttribute);
        }

        // excluded settings
        IEnumerable<IComponentSettingsManager> componentSettingsManagers =
          ResharperSettingsManager.Instance.ComponentSettingsManagers;
        IEnumerable<IComponentSettingsManager> excludedManagers =
          componentSettingsManagers.Where(m => !SettingsManagers.Contains(m));

        if (excludedManagers.Any())
        {
          XmlElement excludedSettingsElement =
            document.CreateElement(Resources.SettingsFileExcludedSettingsGroupsElementName);
          settingsElement.AppendChild(excludedSettingsElement);

          foreach (var manager in excludedManagers)
          {
            XmlElement element = document.CreateElement(Resources.SettingsFileExcludedSettingsGroupElementName);
            excludedSettingsElement.AppendChild(element);

            XmlAttribute attribute = document.CreateAttribute(Resources.SettingsFileExcludedSettingsGroupNameAttribute);
            attribute.Value = manager.ComponentTagName;
            element.Attributes.Append(attribute);
          }
        }

        if (SettingsManagersOptions.Any())
        {
          XmlElement settingsManagersOptionsElement =
            document.CreateElement(Resources.SettingsFileSettingsGroupsOptionsElementName);
          settingsElement.AppendChild(settingsManagersOptionsElement);

          foreach (KeyValuePair<string, object> options in SettingsManagersOptions)
          {
            var settingsManager = SettingsManagers.Where(m => m.ComponentTagName == options.Key).FirstOrDefault();

            if (settingsManager == null)
            {
              continue;
            }

            XmlElement optionsElement = document.CreateElement(options.Key);
            settingsManagersOptionsElement.AppendChild(optionsElement);

            settingsManager.SaveOptions(optionsElement, options.Value);
          }
        }

        UpdateReadOnlyFlag(Path.FullPath);

        document.Save(Path.FullPath);
      }
      catch (Exception ex)
      {
        MessageBoxExtensions.ShowError(ex, "Error", "An error occurred while saving the settings file.");

        return false;
      }

      return true;
    }
    


    /// <summary>
    /// Saves the resharper settings.
    /// </summary>
    /// <returns>
    /// Returns an instance of <see cref="System.Boolean"/>.
    /// </returns>
    public bool SaveResharperSettings()
    {
      return SaveResharperSettings(ResharperSettingsManager.Instance.ComponentSettingsManagers);
    }


    /// <summary>
    /// Saves the resharper settings.
    /// </summary>
    /// <param name="settingsManagers">
    /// The settings managers.
    /// </param>
    /// <returns>
    /// Returns an instance of <see cref="System.Boolean"/>.
    /// </returns>
    public virtual bool SaveResharperSettings([NotNull] IEnumerable<IComponentSettingsManager> settingsManagers)
    {
      try
      {
        var xmlDocument = new XmlDocument();

        if (File.Exists(ExternalSettingsContainerAbsoluteFilePath.FullPath))
        {
          xmlDocument.Load(ExternalSettingsContainerAbsoluteFilePath.FullPath);
        }

        XmlElement settingsElement = GetResharperSettingsNode(xmlDocument);

        settingsManagers = settingsManagers.Where(m => SettingsManagers.Contains(m));
        ResharperSettingsManager.Instance.SaveResharperSettings(xmlDocument, settingsElement, settingsManagers, SettingsManagersOptions);

        UpdateReadOnlyFlag(ExternalSettingsContainerAbsoluteFilePath.FullPath);

        xmlDocument.Save(ExternalSettingsContainerAbsoluteFilePath.FullPath);

        return true;
      }
      catch (Exception exception)
      {
        var stringBuilder = new StringBuilder();
        stringBuilder.AppendLine("An error occurred while saving settings file.");

        while (exception != null)
        {
          stringBuilder.AppendLine();
          stringBuilder.AppendLine(exception.Message);
          stringBuilder.AppendLine();
          stringBuilder.AppendLine("Stack trace:");
          stringBuilder.AppendLine(exception.StackTrace);

          exception = exception.InnerException;
        }

        MessageBox.ShowError(stringBuilder.ToString(), "Error");
      }

      return false;
    }


    /// <summary>
    /// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
    /// </summary>
    /// <returns>
    /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
    /// </returns>
    public override string ToString()
    {
      return Name;
    }


    /// <summary>
    /// Updates the common resharper settings.
    /// </summary>
    /// <returns>
    /// Returns an instance of <see cref="System.Boolean"/>.
    /// </returns>
    public bool UpdateCommonResharperSettings()
    {
      return UpdateCommonResharperSettings(ResharperSettingsManager.Instance.ComponentSettingsManagers);
    }


    /// <summary>
    /// Updates the common settings.
    /// </summary>
    /// <param name="settingsManagers">
    /// The settings managers.
    /// </param>
    /// <returns>
    /// Returns an instance of <see cref="System.Boolean"/>.
    /// </returns>
    public virtual bool UpdateCommonResharperSettings([NotNull] IEnumerable<IComponentSettingsManager> settingsManagers)
    {
      try
      {
        if (File.Exists(ExternalSettingsContainerAbsoluteFilePath.FullPath))
        {
          var xmlDocument = new XmlDocument();
          xmlDocument.Load(ExternalSettingsContainerAbsoluteFilePath.FullPath);

          XmlElement settingsNode = xmlDocument.GetResharperSettingsNode();

          if (settingsNode != null)
          {
            settingsManagers = settingsManagers.Where(m => SettingsManagers.Contains(m));

            if (!ResharperSettingsManager.Instance.UpdateResharperSettings(settingsNode, settingsManagers, SettingsManagersOptions))
            {
              return true;
            }

            UpdateReadOnlyFlag(ExternalSettingsContainerAbsoluteFilePath.FullPath);

            xmlDocument.Save(ExternalSettingsContainerAbsoluteFilePath.FullPath);
          }
        }

        return true;
      }
      catch (Exception exception)
      {
        var stringBuilder = new StringBuilder();
        stringBuilder.AppendLine("An error occurred while saving settings file.");

        while (exception != null)
        {
          stringBuilder.AppendLine();
          stringBuilder.AppendLine(exception.Message);
          stringBuilder.AppendLine();
          stringBuilder.AppendLine("Stack trace:");
          stringBuilder.AppendLine(exception.StackTrace);

          exception = exception.InnerException;
        }

        MessageBox.ShowError(stringBuilder.ToString(), "Error");
      }

      return false;
    }

    #endregion


    #region Methods

    /// <summary>
    /// Loads the contents.
    /// </summary>
    /// <returns>
    /// Returns an instance of <see cref="System.Boolean"/>.
    /// </returns>
    protected bool LoadContents()
    {
      Exception exception = null;

      for (int i = 0; i < 3; i++)
      {
        try
        {
          var document = new XmlDocument();
          document.Load(Path.FullPath);

          XmlElement settingsNode = document.GetSharingSettingsNode();

          if (settingsNode == null)
          {
            XmlElement resharperSettingsNode = document.GetResharperSettingsNode();

            if (resharperSettingsNode == null)
            {
              string text = string.Format(
                CultureInfo.InvariantCulture, "Failed to load settings file '{0}'. Invalid file content.", Path);

              MessageBox.ShowError(text, "Error");

              return false;
            }

            return true;
          }

          if (settingsNode.HasAttribute(Resources.AutoUpdateCommonSettingsAttributeName))
          {
            string value = settingsNode.GetAttribute(Resources.AutoUpdateCommonSettingsAttributeName);

            AutoUpdateCommonSettings = value.ToLower() == "true";
          }

          if (settingsNode.HasAttribute(Resources.ClearReadOnlyFlagOnUpdatesAttributeName))
          {
            string value = settingsNode.GetAttribute(Resources.ClearReadOnlyFlagOnUpdatesAttributeName);

            ClearReadOnlyFlagOnUpdates = value.ToLower() == "true";
          }

          if (settingsNode.HasAttribute(Resources.ExternalSettingsFilePathAttributeName))
          {
            string value = settingsNode.GetAttribute(Resources.ExternalSettingsFilePathAttributeName);

            ExternalSettingsContainerFilePath = new FileSystemPath(value);

            if (!File.Exists(ExternalSettingsContainerAbsoluteFilePath.FullPath))
            {
              string text = string.Format(
                CultureInfo.InvariantCulture, 
                "Failed to load solution settings. The external settings file '{0}' could not be found.", 
                ExternalSettingsContainerAbsoluteFilePath.FullPath);

              MessageBox.ShowError(text, "Error");

              return false;
            }
          }

          var excludedeManagers = new List<IComponentSettingsManager>();
          IEnumerable<IComponentSettingsManager> settingsManagers =
            ResharperSettingsManager.Instance.ComponentSettingsManagers;

          XmlElement excludedSettingsElement = settingsNode[Resources.SettingsFileExcludedSettingsGroupsElementName];
          if (excludedSettingsElement != null)
          {
            foreach (XmlElement element in excludedSettingsElement.ChildNodes)
            {
              if (element.LocalName != Resources.SettingsFileExcludedSettingsGroupElementName)
              {
                continue;
              }

              XmlAttribute attribute = element.Attributes[Resources.SettingsFileExcludedSettingsGroupNameAttribute];

              if (attribute == null)
              {
                continue;
              }

              IComponentSettingsManager manager =
                settingsManagers.Where(m => m.ComponentTagName == attribute.Value).FirstOrDefault();

              if (manager != null)
              {
                excludedeManagers.Add(manager);
              }
            }

            SettingsManagers = settingsManagers.Where(m => !excludedeManagers.Contains(m));
          }

          XmlElement settingsManagersOptionsElement = settingsNode[Resources.SettingsFileSettingsGroupsOptionsElementName];

          if (settingsManagersOptionsElement != null)
          {
            SettingsManagersOptions.Clear();

            foreach (var settingsManager in SettingsManagers)
            {
              var optionsElement = settingsManagersOptionsElement[settingsManager.ComponentTagName];

              if (optionsElement == null)
              {
                continue;
              }

              var options = settingsManager.LoadOptions(optionsElement);

              if (options != null)
              {
                SettingsManagersOptions.Add(settingsManager.ComponentTagName, options);
              }
            }
          }

          exception = null;

          break;
        }
        catch (IOException ex)
        {
          exception = ex;
        }
        catch (Exception ex)
        {
          exception = ex;

          break;
        }

        Thread.Sleep(new TimeSpan(0, 0, 0, 3));
      }

      if (exception == null)
      {
        return true;
      }

      MessageBoxExtensions.ShowError(exception, "Error", "An error occurred while loading shared settings file.");

      return false;
    }


    [NotNull]
    private static XmlElement GetResharperSettingsNode([NotNull] XmlDocument document)
    {
      XmlElement rootElement = document.GetRootSettingsFileNode();

      if (rootElement == null)
      {
        XmlComment comment = document.CreateComment(Resources.SettingsFileHeaderComment);
        document.AppendChild(comment);

        rootElement = document.CreateElement(Resources.SettingsFileRootNodeName);
        document.AppendChild(rootElement);
      }

      XmlElement settingsElement = document.GetResharperSettingsNode();

      if (settingsElement == null)
      {
        settingsElement = rootElement.CreateElement(Resources.SettingsFileResharperSettingsNodeName);

        rootElement.AppendChild(settingsElement);
      }

      return settingsElement;
    }


    [NotNull]
    private static XmlElement GetSharingSettingsNode([NotNull] XmlDocument document)
    {
      XmlElement rootElement = document.GetRootSettingsFileNode();

      if (rootElement == null)
      {
        rootElement = document.CreateElement(Resources.SettingsFileRootNodeName);
        document.AppendChild(rootElement);
      }

      XmlElement settingsElement = document.GetSharingSettingsNode();

      if (settingsElement != null)
      {
        settingsElement.RemoveAll();

        return settingsElement;
      }

      settingsElement = rootElement.CreateElement(Resources.SettingsFileSharingSettingsNodeName);
      rootElement.AppendChild(settingsElement);

      return settingsElement;
    }


    private void UpdateReadOnlyFlag([NotNull] string filePath)
    {
      if (!File.Exists(filePath) || !ClearReadOnlyFlagOnUpdates)
      {
        return;
      }

      FileAttributes attributes = File.GetAttributes(filePath);

      if (attributes.HasAny(FileAttributes.ReadOnly))
      {
        attributes &= ~FileAttributes.ReadOnly;
      }

      File.SetAttributes(filePath, attributes);
    }

    #endregion
  }
}